package com.angle3d;

import android.opengl.GLES20;


/**
 * Class representing a mesh of vertices (3D geometry).
 * @author Alessandro Boccalatte
 * @see {@link VertexBuffer}
 * @see {@link MeshUtil}
 */
public class Mesh extends SceneObject {
	
	/**
	 * Primitive type for the mesh.
	 * These map to the primitive types supported by OpenGL ES 2.0.
	 */
	public enum Primitive
	{
	    POINTS,
	    LINES,
	    LINE_STRIP,
	    LINE_LOOP,
	    TRIANGLES,
	    TRIANGLE_STRIP,
	    TRIANGLE_FAN
	}
	
	/**
	 * Face cull mode for the mesh. One of:
	 * - NONE, for no face culling (default)
	 * - BACK, enabling face culling for back faces
	 * - FRONT, enabling face culling for front faces
	 * - FRONT_AND_BACK, to cull both front and back faces (this results in invisible meshes)
	 */
	public enum CullMode {
		NONE,
		BACK,
		FRONT,
		FRONT_AND_BACK
	}
	
	static class NamedTexture {
		NamedTexture(String shaderTextureName, Texture texture) {
			this.mShaderTextureName = shaderTextureName;
			this.mTexture = texture;
		}
		private final String mShaderTextureName;
		private final Texture mTexture;
		String getShaderTextureName() { return mShaderTextureName; }
		Texture getTexture() { return mTexture; }
	}
	
	static class NamedMatrix {
		NamedMatrix(String shaderMatrixName, Matrix4 matrix) {
			this.mShaderMatrixName = shaderMatrixName;
			this.mMatrix = matrix;
		}
		private final String mShaderMatrixName;
		private final Matrix4 mMatrix;
		String getShaderMatrixName() { return mShaderMatrixName; }
		Matrix4 getMatrix() { return mMatrix; }
	}
	
	private final BoundingBox mBox = new BoundingBox();
	private Primitive mPrimitive = Primitive.TRIANGLES;
	private CullMode mCullMode = CullMode.NONE;
	private float mLineWidth = 1;
	private VertexBuffer mVertexBuffer = null;
	private final NamedTexture[] mTextures = new NamedTexture[Texture.MAX_TEXTURE_UNITS];
	private final NamedMatrix[] mTexMatrices = new NamedMatrix[Texture.MAX_TEXTURE_UNITS];
	
	/**
	 * Creates an empty Mesh.
	 */
	public Mesh() {
		super();
		for (int i = 0; i < Texture.MAX_TEXTURE_UNITS; ++i) {
			mTextures[i] = null;
			mTexMatrices[i] = new NamedMatrix("TextureMatrix" + i, Matrix4.identity());
		}
	}
	
	/**
	 * Returns the bounding box of the mesh.
	 * Note that the bounding box is not computed automatically;
	 * therefore it is normally the responsibility of the user to set it properly if a use of this feature is foreseen.
	 * @return the bounding box of the mesh.
	 */
	public BoundingBox getBoundingBox() {
		return mBox;
	}
	
	/**
	 * Specifies the primitive used by the mesh to link the vertices.
	 * @param primitive the primitive used by the mesh.
	 * @see #getPrimitive()
	 */
	public void setPrimitive(Primitive primitive) {
		mPrimitive = primitive;
	}
	
	/**
	 * Returns the primitive used by the mesh.
	 * @return the primitive used by the mesh.
	 * @see #setPrimitive(Primitive)
	 */
	public Primitive getPrimitive() {
		return mPrimitive;
	}
	
	/**
	 * Sets the face cull mode to be used when rendering this mesh. 
	 * @param mode the face cull mode.
	 * @see #getCullMode()
	 */
	public void setCullMode(CullMode mode) {
		mCullMode = mode;
	}
	
	/**
	 * Returns the face cull mode for this mesh.
	 * @return the face cull mode.
	 * @see #setCullMode(CullMode)
	 */
	public CullMode getCullMode() {
		return mCullMode;
	}
	
	/**
	 * Sets the {@link VertexBuffer} used by the mesh.
	 * @param buffer a VertexBuffer.
	 */
	public void setVertexBuffer(VertexBuffer buffer) {
		mVertexBuffer = buffer;
	}
	
	/**
	 * Returns the {@link VertexBuffer} used by the mesh.
	 * @return the VertexBuffer used by the mesh.
	 */
	public VertexBuffer getVertexBuffer() {
		return mVertexBuffer;
	}
	
	/**
	 * Sets the line thickness (width) to be used in this mesh for rendering line elements.
	 * @param width the line width
	 * @see #getLineWidth()
	 */
	public void setLineWidth(float width) {
		mLineWidth = width;
	}
	
	/**
	 * Returns the line thickness (width) to be used in this mesh for rendering line elements.
	 * @return the line width.
	 * @see #setLineWidth(float)
	 */
	public float getLineWidth() {
		return mLineWidth;
	}
	
	/**
	 * Sets the texture used by the mesh, for a given name and texture unit (unit must be 0 or 1)
	 * @param texture the texture
	 * @param shaderTextureName the name of the uniform texture parameter as it appears in the shader source code 
	 * @param unit the texture unit
	 * @see #getTexture(int)
	 */
	public void setTexture(Texture texture, String shaderTextureName, int unit) {
		if (unit < 0 || unit >= Texture.MAX_TEXTURE_UNITS) {
			return;
		}
		mTextures[unit] = new NamedTexture(shaderTextureName, texture);
	}
	
	/**
	 * Returns the texture used by the mesh for the given texture unit
	 * @param unit the texture unit
	 * @return the texture for the given texture unit.
	 * @see #setTexture(Texture, int)
	 */
	public Texture getTexture(int unit) {
		if (unit < 0 || unit >= Texture.MAX_TEXTURE_UNITS) {
			return null;
		}
		return mTextures[unit] != null ? mTextures[unit].getTexture() : null;
	}
	
	/**
	 * Sets the texture transformation matrix for a given name and texture unit.
	 * @param matrix a texture transformation matrix
	 * @param shaderMatrixName the name of the uniform texture parameter as it appears in the shader source code
	 * @param unit a texture unit
	 * @see #getTextureMatrix(int)
	 */
	public void setTextureMatrix(Matrix4 matrix, String shaderMatrixName, int unit) {
		if (unit < 0 || unit >= Texture.MAX_TEXTURE_UNITS) {
			return;
		}
		mTexMatrices[unit] = new NamedMatrix(shaderMatrixName, matrix);
	}
	
	/**
	 * Returns the texture transformation matrix for a given texture unit.
	 * @param unit a texture unit
	 * @return the texture transformation matrix for the given texture unit.
	 * @see #setTextureMatrix(Matrix4, int)
	 */
	public Matrix4 getTextureMatrix(int unit) {
		if (unit < 0 || unit >= Texture.MAX_TEXTURE_UNITS) {
			return Matrix4.identity();
		}
		return mTexMatrices[unit] != null ? mTexMatrices[unit].getMatrix() : Matrix4.identity();
	}
	
	@Override
	public void render(View3D view, Matrix4 projection, Matrix4 modelview) {
		super.render(view, projection, modelview);
		
		if (mShader != null) 
		{
			//Apply textures and texture matrices
			Texture.unbindAll();
			for (int i = 0; i < Texture.MAX_TEXTURE_UNITS; ++i)
			{
				if (mTexMatrices[i] != null) {
					mShader.setUniformValue(mTexMatrices[i].getShaderMatrixName(), mTexMatrices[i].getMatrix());
				}
				if (mTextures[i] != null && mTextures[i].getTexture() != null) {
					mTextures[i].getTexture().bind(mShader, mTextures[i].getShaderTextureName(), i);
				}
			}
			
			//Render geometry
			renderGeometry(mShader);
		}
	}
	
	protected void renderGeometry(ShaderProgram shader) {
		if (mLineWidth > 1) {
			GLES20.glLineWidth(mLineWidth);
		}
		
		if (mCullMode != CullMode.NONE) {
			GLES20.glEnable(GLES20.GL_CULL_FACE);
			GLES20.glCullFace(getGLCullMode(mCullMode));
		}
		
		mVertexBuffer.bind(shader);
		GLES20.glDrawElements(getGLPrimitive(mPrimitive), 
							  mVertexBuffer.getIndexCount(), 
							  GLES20.GL_UNSIGNED_SHORT, 
							  mVertexBuffer.getIndices());
		mVertexBuffer.unbind(shader);
		
		if (mCullMode != CullMode.NONE) {
			GLES20.glDisable(GLES20.GL_CULL_FACE);//restore default
		}
		
		if (mLineWidth > 1) {
			GLES20.glLineWidth(1);//restore default line width
		}
	}
	
	private static int getGLCullMode(CullMode mode) {
		switch(mode) {
		case NONE: return GLES20.GL_NONE;
		case BACK: return GLES20.GL_BACK;
		case FRONT: return GLES20.GL_FRONT;
		case FRONT_AND_BACK: return GLES20.GL_FRONT_AND_BACK;
		}
		return GLES20.GL_NONE;
	}
	
	private static int getGLPrimitive(Primitive primitive) {
		switch(primitive)
		{
		case POINTS: return GLES20.GL_POINTS;
		case LINES: return GLES20.GL_LINES;
		case LINE_LOOP: return GLES20.GL_LINE_LOOP;
		case LINE_STRIP: return GLES20.GL_LINE_STRIP;
		case TRIANGLES: return GLES20.GL_TRIANGLES;
		case TRIANGLE_FAN: return GLES20.GL_TRIANGLE_FAN;
		case TRIANGLE_STRIP: return GLES20.GL_TRIANGLE_STRIP;
		}
		return GLES20.GL_POINTS;
	}
}

